從 Angular 17 開始,團隊開始將 decorators 遷移到 signals。第一個是 @Input
decorator 和對應的 signal input 函數。 此後以下查詢裝飾器已移轉。
今天,我將介紹 viewChild
,因為我經常將它與表單、HTML 元素、ngTemplates 和組件一起使用。
viewChild
函數傳回一個 signalstatic
屬性從 viewChild 函數中刪除viewChild.required()
表示該元素在組件中至少出現一次,並且檢索第一個元素。 此函數的傳回類型是 Signal。viewChild()
表示該元素未出現在範本中且可能是未定義的。函數的傳回類型為 Signal<T | undefined>。在以下用例中,我將展示 viewChild 如何透過範本變數 (template variable) 查詢 NgForm、ngTemplates, directives 和 Angular 組件。
@Component({
selector: 'app-address-form',
standalone: true,
imports: [FormsModule, JsonPipe],
template: `
<h3>ViewChild to obtain a NgForm and ElementRef</h3>
<form #f="ngForm">
<div>
<label for="firstName">
<span>First Name: </span>
<input id="firstName" name="firstName" type="text" required [(ngModel)]="formValues().firstName" size="50" #fn="ngModel">
</label>
@if(fn.dirty && fn.errors?.['required']) {
<p class="error">First name is required.</p>
}
</div>
<div>
<label for="lastName">
<span>Last Name:: </span>
<input id="lastName" name="lastName" type="text" required [(ngModel)]="formValues().lastName" size="50" #ln="ngModel">
</label>
@if(ln.dirty && ln.errors?.['required']) {
<p class="error">Last name is required.</p>
}
</div>
</form>
`,
})
export class AddressFormComponent implements OnInit {}
AddressFormComponent
是一個簡單的 template-driven 表單,帶有名字和姓氏輸入欄位。
export type FormValues = {
firstName: string;
lastName: string;
}
export const initialValues = {
firstName: 'test',
lastName: 'me',
}
export class AddressFormComponent implements OnInit {
form = viewChild.required('f', { read: NgForm });
formValues = signal<FormValues>(initialValues);
isFormValid = signal(false);
isFormSubmitted = signal(false);
}
viewChild.required
函數從範本中查詢 NgForm
。 Selector是 f
,因為 NgForm
的範本變數 (template variable) 是 #f
。 第二個參數 { read: NgForm }
確保檢索 NgForm
。 formValues
是儲存表單值的 signal
。 isFormValid
signal 儲存表單是否有效。 isFormSubscribed
signal 儲存表單是否已提交。
constructor() {
effect((OnCleanUp) => {
const formValueChanges$ = this.form().form.valueChanges.pipe(
debounceTime(0)
);
const sub = formValueChanges$.subscribe((values) => {
this.formValues.set(values);
this.isFormValid.set(this.form().valid || false);
this.isFormSubmitted.set(false);
});
const sub2 = this.form().ngSubmit.subscribe(() => {
this.isFormSubmitted.set(true);
});
OnCleanUp(() => {
sub.unsubscribe();
sub2.unsubscribe();
});
});
}
當表單值更新時, effect
執行邏輯來設定 signal 的值。
viewModel = computed(() => ({
values: this.formValues(),
isFormValid: this.isFormValid(),
isFormSubmitted: this.isFormSubmitted(),
}));
get vm() {
return this.viewModel();
}
建立 view model 以存取 HTML 範本中的 signal 值。
<form #f="ngForm">
<button type="submit" [disabled]="!vm.isFormValid">Submit</button>
</form>
@if (vm.isFormSubmitted) {
<pre>
values: {{ vm.values | json }}
</pre>
}
當 vm.isFormValid
為 true
時,表單有效,並且啟用 Submit
按鈕。當 vm.isFormSubmitted
為 true
時,表單提交發生並顯示表單物件值。
@Component({
selector: 'app-permission',
standalone: true,
imports: [FormsModule, NgTemplateOutlet],
template: `
<ng-template #admin>
<p>You are an admin. You can do the following task(s):</p>
<ul>
<li>Add an account</li>
<li>Delete an account</li>
<li>Update an account</li>
<li>Upgrade role</li>
<li>Downgrade role</li>
</ul>
</ng-template>
<ng-template #user>
<p>You are a user. You can do the following task(s):</p>
<ul>
<li>View accounts</li>
</ul>
</ng-template>
<ng-container *ngTemplateOutlet="template()" />
`,
})
export default class PermissionComponent {
hasPermission = signal(false);
}
此例子將單選按鈕綁定到 hasPermission
signal。 當 hasPermission
為 true
時,組件顯示 admin
ngTemplate。當 hasPermission
為 false
時,組件顯示 user
ngTemplate。
userTemplate = viewChild.required('user', { read: TemplateRef });
viewChild.required
查詢具有範本變數 #user
的元素,並且 { read: TemplateRef }
檢索具有 TemplateRef
類型的元素。
adminTemplate = viewChild.required('admin', { read: TemplateRef });
viewChild.required
使用範本變數 (template variable) #admin
查詢 ngTemplate
。
號
template = computed(() => this.hasPermission() ? this.adminTemplate() : this.userTemplate());
範本根據 hasPermission
signal value 決定 template
computed signal. template
決定要顯示的 ngTemplate。
<ng-container *ngTemplateOutlet="template()" />
將範本指派給 ngTemplateOutlet
directive,由 ngContainer
顯示它。
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
const imgURL = 'https://picsum.photos';
@Component({
selector: 'app-photo',
standalone: true,
template: `
<div class="photo">
<img [src]="img()" alt="Random picture" />
</div>
`,
})
export default class PhotoComponent {
width = signal(300);
height = signal(200);
random = signal(Date.now());
img = computed(() => `${imgURL}/${this.width()}/${this.height()}?random=${this.random()}`)
}
PhotoComponent
組件有三個 signals
來定義圖片 URL 的寬度、高度和隨機種子。 當任何 signal value
更新時,img
computed signal 會產生新的圖像 URL。
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import PhotoComponent from './photo.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [PhotoComponent],
template: `
<h3>ViewChild to obtain PhotoComponent</h3>
<app-photo />
<button (click)="changeImage()">Change image</button>
<button (click)="increaseDimensions()">Make it 400 x 400</button>
`,
})
export class App {
photo = viewChild.required(PhotoComponent);
changeImage() {
const photo = this.photo();
photo.random.set(Date.now());
}
increaseDimensions() {
const photo = this.photo();
photo.width.set(400);
photo.height.set(400);
photo.random.set(Date.now());
}
}
App
組件使用 viewChild
函數按類型查詢 PhotoComponent
。 App
組件有兩個按鈕,點擊時可設定 signal。 然後,`PhotoComponent 在 HTML 範本中載入新圖像
import { Directive, ElementRef, inject, model, OnInit } from "@angular/core";
@Directive({
selector: '[appLabel]',
standalone: true,
host: {
'(mouseenter)' : "updateColor(appLabel())",
'(mouseleave)' : "updateColor('black')"
}
})
export class AppBoldLabelDirective {
nativeElement = inject<ElementRef<HTMLLabelElement>>(ElementRef).nativeElement;
appLabel = model('red');
updateColor(value: string) {
this.nativeElement.style.color = value;
}
}
定義 AppBoldLabelDirective
以變更 mouseenter
和 mouseleave
事件上的標籤顏色。在 PermissionComponent
中,指定標籤元素的 directive.
imports: [FormsModule, NgTemplateOutlet, AppBoldLabelDirective],
template: `
<label for="hasPermission" [appLabel]="'red'">Yes</label><br>
<label for="noPermission" [appLabel]="'darkgoldenrod'">No</label><br>
`,
export default class PermissionComponent implements AfterViewInit {
directive = viewChild(AppBoldLabelDirective);
ngAfterViewInit() {
const directive = this.directive();
if (directive) {
directive.nativeElement.style.color = 'goldenrod';
}
}
}
出於示範原因,該組件會查詢第一個 AppBoldLabelDirective
directive,並且 ngAfterViewInit
lifecycle hook 將文字顏色變更為金黃色。 當滑鼠懸停在標籤上時,標籤文字的顏色會改變。當滑鼠離開標籤時,標籤文字會變成黑色。
結論:
viewChild
可以查詢 elements、ngTemplates、directives 和 components。第一個參數是一個選擇器,它是 template variable 或 type。viewChild
要傳回的元素類型viewChild.required
函數。否則,我們應該使用viewChild
函數,它可以傳回 signal
或 undefined。viewChild
函數的 selector 符合範本中的多個元素時,傳回第一個。viewChild.required
無法查詢某個元素,則函數將拋出錯誤。鐵人賽的第 17 天就這樣結束了。
結論: